/*
 * Copyright 2011 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.javascript.jscomp;

import com.google.javascript.jscomp.testing.TestExternsBuilder;
import com.google.javascript.rhino.Node;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for {#link {@link OptimizeCalls} */
@RunWith(JUnit4.class)
public final class OptimizeCallsIntegrationTest extends CompilerTestCase {

  public OptimizeCallsIntegrationTest() {
    super(
        DEFAULT_EXTERNS
            + """
            var window;
            var goog = {};
            goog.reflect = {};
            goog.reflect.object = function(a, b) {};
            function goog$inherits(a, b) {}
            var alert;
            function use(x) {}
            """);
  }

  @Override
  @Before
  public void setUp() throws Exception {
    super.setUp();
    enableNormalize();
    // TODO(bradfordcsmith): Stop normalizing the expected output or document why it is necessary.
    enableNormalizeExpectedOutput();
    enableGatherExternProperties();
  }

  @Override
  protected CompilerPass getProcessor(final Compiler compiler) {
    return new CompilerPass() {

      @Override
      public void process(Node externs, Node root) {
        new PureFunctionIdentifier.Driver(compiler).process(externs, root);
        new RemoveUnusedCode.Builder(compiler)
            .removeLocalVars(true)
            .removeGlobals(true)
            .build()
            .process(externs, root);

        OptimizeCalls.builder()
            .setCompiler(compiler)
            .setConsiderExterns(false)
            .addPass(new OptimizeReturns(compiler))
            .addPass(new OptimizeParameters(compiler))
            .build()
            .process(externs, root);
      }
    };
  }

  @Test
  public void testUnusedTaggedTemplateLiteralSubstitutionsAreNotRemoved() {
    test(
        """
        var f = function(strings, x, y) {
        // x and y are unused
          return strings;
        };
        f`tagged${1} ${2}`;
        f();
        """,
        """
        var f = function(strings) {
        // x and y are unused
          return strings;
        };
        f`tagged${1} ${2}`;
        f();
        """);

    test(
        """
        var f = function(strings, ...rest) {
        // `rest` is unused
          return strings;
        };
        f`tagged ${1} ${2}`;
        f();
        """,
        """
        var f = function(strings) {
        // `rest` is unused
          return strings;
        };
        f`tagged ${1} ${2}`;
        f();
        """);

    testSame(
        """
        var f = function(strings, ...rest) {
        // all arguments are used
          return [strings, rest];
        };
        f`tagged ${1} ${2}`;
        f();
        """);
  }

  @Test
  public void testConvertTaggedTemplateLiteralToANormalCall() {
    test(
        """
        var f = function(strings, x, y) {
        //  `strings` parameter is unused
          return [x, y];
        };
        f`tagged${1} ${2}`;
        f();
        """,
        """
        var f = function(x$jscomp$1, y) {
          return [x$jscomp$1, y];
        };
        // Tagged template literal gets converted to a normal call
        f(1, 2);
        f();
        """);
  }

  @Test
  public void testConvertTaggedTemplateLiteralToANormalCallThenOptimized() {
    test(
        """
        var f = function(strings, x, y) {
        //  all parameters are unused
          return '';
        };
        f`tagged${1} ${2}`;
        f();
        """,
        """
        var f = function() {
          return '';
        };
        // Tagged template literal gets converted to a normal call
        f();
        f();
        """);
  }

  @Test
  public void testInlineWindow() {
    test(
        """
        function foo(window) {
          alert(window);
        }
        foo(window);
        """,
        """
        function foo(      ) {
          var window$jscomp$1 = window;
          alert(window$jscomp$1);
        }
        foo(      );
        """);
  }

  @Test
  public void testRemoveUnusedConstructorArgumentWithDefaultValues() {
    test(
        externs(new TestExternsBuilder().addConsole().build()),
        srcs(
            """
            class C {
              constructor(unusedValue = () => {}, usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            console.log(new C(() => {}, 25).getValue());
            """),
        expected(
            """
            class C {
              constructor(                                 ) {
                var usedValue = 25;
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            console.log(new C(            ).getValue());
            """));

    test(
        externs(new TestExternsBuilder().addConsole().build()),
        srcs(
            """
            class C {
              constructor(unusedValue = () => {}, usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            // 2 calls to the constructor with different used values,
            // so the second parameter cannot actually be removed.
            console.log(new C(() => {}, 15).getValue());
            console.log(new C(() => {}, 25).getValue());
            """),
        expected(
            """
            class C {
              constructor(                        usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            console.log(new C(          15).getValue());
            console.log(new C(          25).getValue());
            """));
  }

  @Test
  public void testRemoveUnusedConstructorArgumentWithSubClassWithoutConstructor() {
    test(
        externs(new TestExternsBuilder().addConsole().build()),
        srcs(
            """
            class C {
              constructor(unusedValue = () => {}, usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            class SubC extends C {} // no declared constructor
            console.log(new SubC(() => {}, 25).getValue());
            console.log(new C(() => {}, 25).getValue());
            """),
        expected(
            """
            class C {
            // default value removed, but no arguments removed or inlined
            // due to subclass.
              constructor(unusedValue           , usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            class SubC extends C {}
            console.log(new SubC(() => {}, 25).getValue());
            console.log(new C(() => {}, 25).getValue());
            """));
  }

  @Test
  public void testRemoveUnusedConstructorArgumentWithSubClassWithConstructor() {
    test(
        externs(new TestExternsBuilder().addConsole().build()),
        srcs(
            """
            class C {
              constructor(unusedValue = () => {}, usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            class SubC extends C {
              constructor(usedValue) {
                super(0, usedValue); // calls super constructor
              }
            }
            console.log(new SubC(25).getValue());
            console.log(new C(() => {}, 25).getValue());
            """),
        expected(
            """
            class C {
            // First parameter removed because it was never used.
              constructor(                        usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            class SubC extends C {
            // Parameter was inlined with the only value ever passed to it.
              constructor(     ) {
                var usedValue = 25;
            // first parameter to super() was removed to match its removal in the
            // definition above.
                super(   usedValue);
              }
            }
            console.log(new SubC(  ).getValue());
            // unused parameter removed.
            console.log(new C(          25).getValue());
            """));
    // Same test as above, but with class expressions instead of declarations.
    test(
        externs(new TestExternsBuilder().addConsole().build()),
        srcs(
            """
            const C = class { // class expression instead of declaration
              constructor(unusedValue = () => {}, usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            const SubC = class extends C { // class expression instead of declaration
              constructor(usedValue) {
                super(0, usedValue); // calls super constructor
              }
            }
            console.log(new SubC(25).getValue());
            console.log(new C(() => {}, 25).getValue());
            """),
        expected(
            """
            const C = class {
            // First parameter removed because it was never used.
              constructor(                        usedValue = 3) {
                this.value = usedValue;
              }
              getValue() {
                return this.value;
              }
            }
            const SubC = class extends C {
            // Parameter was inlined with the only value ever passed to it.
              constructor(     ) {
                var usedValue = 25;
            // first parameter to super() was removed to match its removal in the
            // definition above.
                super(   usedValue);
              }
            }
            console.log(new SubC(  ).getValue());
            // unused parameter removed.
            console.log(new C(          25).getValue());
            """));
  }

  @Test
  public void testAliasOfAFunction() {
    testSame(
        """
        function foo(arg1) {
          return arg1
        }

        // first definition of bar
        let bar = foo;
        // really calls foo(1)
        // the `1` cannot be inlined because bar is an alias of
        // `foo`
        bar(1); // return value unused
        // redefinition of bar with a function literal
        bar = function(arg1) {
          return arg1 + 1;
        };
        bar(1) // return value unused & same argument
        """);
  }

  @Test
  public void testAliasingAssignment() {
    testSame(
        """
        /** @constructor */
        function MyClass() {
          this.myField = null;
        }

        // This assignment creates an alias, so we can't know all of the callers and cannot
        // safely optimize away `myArgument`.
        MyClass.prototype["myMethod"] =
            MyClass.prototype.myMethod = function (myArgument) {
          if (undefined === myArgument) {
              myArgument = this.myField;
          }
          return "myMethod with argument: " + myArgument;
        };

        function globalMyMethod(oMyClass) {
        // One call to `myMethod` exists, and it doesn't use the optional argument.
          return oMyClass.myMethod();
        }

        // These both escape, so they won't be removed as unused.
        window["MyClass"] = MyClass;
        window["globalMyMethod"] = globalMyMethod;
        """);
  }

  @Test
  public void testSimpleRemoval() {
    // unused parameter value
    test(
        "var foo = (p1)=>{}; foo(1); foo(2)", //
        "var foo = (  )=>{}; foo( ); foo( )");
    test(
        "let foo = (p1)=>{}; foo(1); foo(2)", //
        "let foo = (  )=>{}; foo( ); foo( )");
    test(
        "const foo = (p1)=>{}; foo(1); foo(2)", //
        "const foo = (  )=>{}; foo( ); foo( )");
  }

  @Test
  public void testRemovingReturnCallToFunctionWithUnusedParams() {
    test(
        "function foo() {var x; return x = bar(1)} foo(); function bar(x) {}",
        "function foo() {bar();return} foo(); function bar() {1;}");
    test(
        "function foo() {return} foo(); function bar() {1;}", //
        "function foo() {return} foo()");
  }

  @Test
  public void testNestingFunctionCallWithUnsedParams() {
    test(
        """
        function f1(x) { }
        function f2(x) { }
        function f3(x) { }
        function f4(x) { }
        f3(f1(f2()));
        """,
        """
        function f1(){f2()}
        function f2(){}
        function f3(){f1()}f3()
        """);
  }

  @Test
  public void testUnusedAssignOnFunctionWithUnusedParams() {
    test(
        "var foo = function(a){  }; function bar(){var x;x = foo} bar(); foo(1)",
        "var foo = function( ){1;}; function bar(){             } bar(); foo()");
  }

  @Test
  public void testCallSiteInteraction() {
    testSame("var b=function(){return};b()");
    test(
        "var b=function(c){              return c}; b(1)",
        "var b=function( ){var c = 1; c; return  }; b( )");

    test(
        "var b=function(c){};b.call(null, 1); b(2);", // preserve alignment
        "var b=function( ){};b.call(null   ); b( );");
    test(
        "var b=function(c){};b.apply(null, []);", // preserve alignment
        "var b=function( ){};b.apply(null, []);");

    test(
        "var b=function(c){return};b(1);b(2)", //
        "var b=function(){return};b();b();");
    test(
        "var b=function(c){return};b(1,2);b();", //
        "var b=function(){return};b();b();");
    test(
        "var b=function(c){return};b(1,2);b(3,4)", //
        "var b=function(){return};b();b()");

    // Here there is a unknown reference to the function so we can't
    // change the signature.
    // TODO(johnlenz): replace unused parameter values, even
    // if we don't know all the uses.
    testSame("var b=function(c,d){return d};b(1,2);b(3,4);b.f()");

    test(
        "var b=function(c){return};b(1,2);b(3,new use())",
        "var b=function( ){return};b(   );b(  new use())");

    test(
        "var b=function(c){return};b(1,2);b(new use(),4)",
        "var b=function( ){return};b(   );b(new use()  )");

    test(
        "var b=function(c,d){return d};b(1,2);use(b(new use(),4))",
        "var b=function(c,d){return d};b(0,2);use(b(new use(),4))");

    test(
        "var b=function(c,d,e){return d};b(1,2,3);use(b(new use(),4,new use()))",
        "var b=function(c,d  ){return d};b(0,2  );use(b(new use(),4,new use()))");

    test(
        "var a=0; var b=function(c,d){return d};b((a++, 1),2);use(b(new use(),4));use(a)",
        "var a=0;var b=function(c,d){return d};b((a++, 0),2);use(b(new use(),4));use(a)");

    test(
        "var a=0;var b=function(c,d){return d};b((a++,a++,1),2);use(b(new use(),4));use(a)",
        "var a=0;var b=function(c,d){return d};b((a++,a++,0),2);use(b(new use(),4));use(a)");

    testSame(
        "var a=0; var b=function(c,d){return d};b((a++, 1, a++),2);use(b(new use(),4));use(a)");

    // Recursive calls are OK.
    test(
        "var b=function(c,d){b(1,2);return d};b(3,4);use(b(5,6))",
        "var b=function(d){b(2);return d};b(4);use(b(6))");

    testSame("var b=function(c){return arguments};b(1,2);use(b(3,4))");

    // remove all function arguments
    test(
        "var b=function(c,d){return};b();b(1);b(1,2);b(1,2,3)",
        "var b=function(){return};b();b();b();b();");

    // remove no function arguments
    testSame("var b=function(c,d){use(c+d)};b(2,3);b(1,2)");

    // remove some function arguments
    test(
        "var b=function(e,f,c,d){use(c+d)};b(1,2,3,4);b(3,4,5,6)",
        "var b=function(c,d){use(c+d)};b(3,4);b(5,6)");
    test(
        "var b=function(c,d,e,f){use(c+d)};b(1,2);b(3,4)",
        "var b=function(c,d){use(c+d)};b(1,2);b(3,4)");
    test(
        "var b=function(e,c,f,d,g){use(c+d)};b(1,2);b(3,4)",
        "var b=function(c){var f;var d;use(c+d)};b(2);b(4)");
    test(
        "var b=function(c,d){};var b=function(e,f){};b(1,2)",
        "var b=function(){};var b=function(){};b()");
  }

  @Test
  public void nullishCoalesce() {
    testSame("var b = function(c) { use(c) } ?? function(c) { use(c) }; b(1)");
    testSame("var b; b = function(c) { use(c) } ?? function(c) { use(c) }; b(1)");
    testSame("var b = function(c) { use(c) } ?? function(c) { use(c) }; b(1); b(2);");
    testSame("var b; b = function(c) { use(c) } ?? function(c) { use(c) }; b(1); b(2);");
    test(
        "var f = (function(){ return 1; }) ?? (function(...p2){}); f()",
        "var f = (function(){ return  ; }) ?? (function(     ){}); f()");
  }

  @Test
  public void testComplexDefinition1() {
    testSame("var x; var b = x ? function(c) { use(c) } : function(c) { use(c) }; b(1)");
    test(
        "var x; var b = (x, function(c) {            use(c) }); b(1)",
        "var x; var b = (x, function( ) { var c = 1; use(c) }); b()");
    testSame("var x; var b; b = x ? function(c) { use(c) } : function(c) { use(c) }; b(1)");
    test(
        "var x; var b; b = (x, function(c) {            use(c) }); b(1)",
        "var x; var b; b = (x, function( ) { var c = 1; use(c) }); b( )");
  }

  @Test
  public void testComplexDefinition2() {
    testSame("var x; var b = x ? function(c) { use(c) } : function(c) { use(c) }; b(1); b(2);");
    testSame("var x;var b = (x, function(c) { use(c) }); b(1); b(2);");
    testSame("var x; var b; b = x ? function(c) { use(c) } : function(c) { use(c) }; b(1); b(2);");
    testSame("var x; var b; b = (x, function(c) { use(c) }); b(1); b(2);");
  }

  @Test
  public void testCallSiteInteraction_constructors0() {
    // Unused parmeters to constructors invoked with .call
    // can be removed.
    test(
        """
        var Ctor1=function(a,b){return a}; // preserve newlines
        Ctor1.call(this, 1, 2);
        Ctor1(3, 4)
        """,
        """
        var Ctor1=function(a  ){a; return}; // preserve newlines
        Ctor1.call(this,1);
        Ctor1(3)
        """);
  }

  @Test
  public void testCallSiteInteraction_constructors1() {
    // NOTE: Ctor1 used trailing parameter is removed by
    // RemoveUnusedCode

    // For now, goog.inherits prevents optimizations
    test(
        """
        var Ctor1=function(a,b){use(a)};
        var Ctor2=function(x,y){};
        goog$inherits(Ctor2, Ctor1);
        new Ctor2(1,2);new Ctor2(3,4);
        """,
        """
        var Ctor1=function(a){use(a)};
        var Ctor2=function(){};
        goog$inherits(Ctor2, Ctor1);
        new Ctor2(1,2);new Ctor2(3,4);
        """);
  }

  @Test
  public void testCallSiteInteraction_constructors2() {
    // For now, goog.inherits prevents call site optimizations
    String code =
        """
        var Ctor1=function(a,b){return a};
        var Ctor2=function(x,y){Ctor1.call(this,x,y)};
        goog$inherits(Ctor2, Ctor1);
        new Ctor2(1,2);new Ctor2(3,4)
        """;

    String expected =
        """
        var Ctor1=function(a){return a};
        var Ctor2=function(x,y){Ctor1.call(this,x,y)};
        goog$inherits(Ctor2, Ctor1);
        new Ctor2(1,2);new Ctor2(3,4)
        """;

    test(code, expected);
  }

  @Test
  public void testFunctionArgRemovalCausingInconsistency() {
    // Test the case where an unused argument is removed and the argument
    // contains a call site in its subtree (will cause the call site's parent
    // pointer to be null).
    test(
        """
        var a=function(x,y){};
        var b=function(z){};
        a(new b, b)
        """,
        """
        var a=function(){new b;b};
        var b=function(){};
        a()
        """);
  }

  @Test
  public void testRemoveUnusedVarsPossibleNpeCase() {
    test(
        """
        var a = [];
        var register = function(callback) {a[0] = callback};
        register(function(transformer) {});
        register(function(transformer) {});
        """,
        "var register=function(){};register();register()");
  }

  @Test
  public void testDoNotOptimizeJSCompiler_renameProperty() {
    // Only the function definition can be modified, none of the call sites.
    test(
        """
        function JSCompiler_renameProperty(a) {};
        JSCompiler_renameProperty('a');
        """,
        """
        function JSCompiler_renameProperty() {};
        JSCompiler_renameProperty('a');
        """);
  }

  @Test
  public void testFunctionArgRemovalFromCallSites() {
    // remove all function arguments
    test(
        "var b=function(c,d){return};b(1,2);b(3,4)", //
        "var b=function(){return};b();b()");

    // remove no function arguments
    testSame("var b=function(c,d){return c+d};b(1,2);use(b(3,4))");
    test(
        "var b=function(e,f,c,d){return c+d};b(1,2,3,4);use(b(4,3,2,1))",
        "var b=function(c,d){return c+d};b(3,4);use(b(2,1))");

    // remove some function arguments
    test(
        "var b=function(c,d,e,f){use(c+d)};b(1,2);b();",
        "var b=function(c,d){use(c+d)};b(1,2);b();");
    test(
        "var b=function(e,c,f,d,g){use(c+d)};b(1,2);b(3,4,5,6)",
        "var b=function(c,d){use(c+d)};b(2);b(4,6)");
  }

  @Test
  public void testFunctionArgRemovalFromCallSitesSpread1() {
    test(
        "function f(a,b,c,d){};f(...[1,2,3,4]);f(4,3,2,1)", //
        "function f(){};f();f()");
    test(
        "function f(a,b,c,d){};f(...[1,2,3,4], alert());f(4,3,2,1)",
        "function f(){};f(alert());f()");
    test(
        "function f(a,b,c,d){use(c+d)};f(...[1,2,3,4]);f(4,3,2,1)",
        "function f(a,b,c,d){use(c+d)};f(...[1,2,3,4]);f(0,0,2,1)");
    test(
        "function f(a,b,c,d){use(c+d)};f(1,...[2,3,4,5]);f(4,3,2,1)",
        "function f(  b,c,d){use(c+d)};f(  ...[2,3,4,5]);f(  0,2,1)");
    test(
        "function f(a,b,c,d){use(c+d)};f(1,2,...[3,4,5]);f(4,3,2,1)",
        "function f(    c,d){use(c+d)};f(    ...[3,4,5]);f(    2,1)");
    test(
        "function f(a,b,c,d){use(c+d)}; f(...[],2,3);f(4,3,2,1)",
        "function f(a,b,c,d){use(c+d)}; f(...[],2,3);f(0,0,2,1)");
  }

  @Test
  public void testFunctionArgRemovalFromCallSitesSpread2() {
    test(
        "function f(a,b,c,d){};f(...[alert()]);f(4,3,2,1)", //
        "function f(){};f(...[alert()]);f()");
    test(
        "function f(a,b,c,d){};f(...[alert()], alert());f(4,3,2,1)",
        "function f(){};f(...[alert()], alert());f()");
    test(
        "function f(a,b,c,d){use(c+d)};f(...[alert()]);f(4,3,2,1)",
        "function f(a,b,c,d){use(c+d)};f(...[alert()]);f(0,0,2,1)");
    test(
        "function f(a,b,c,d){use(c+d)};f(1,...[alert()]);f(4,3,2,1)",
        "function f(  b,c,d){use(c+d)};f(  ...[alert()]);f(  0,2,1)");
    test(
        "function f(a,b,c,d){use(c+d)};f(1,2,...[alert()]);f(4,3,2,1)",
        "function f(    c,d){use(c+d)};f(    ...[alert()]);f(    2,1)");
    test(
        "function f(a,b,c,d){use(c+d)}; f(...[alert()],2,3);f(4,3,2,1)",
        "function f(a,b,c,d){use(c+d)}; f(...[alert()],2,3);f(0,0,2,1)");
  }

  @Test
  public void testFunctionArgRemovalFromCallSitesSpread3() {
    test(
        "function f(a,b,c,d){};f(...alert());f(4,3,2,1)", //
        "function f(){};f(...alert());f()");
    test(
        "function f(a,b,c,d){};f(...alert(), 1);f(4,3,2,1)", //
        "function f(){};f(...alert());f()");
    test(
        "function f(a,b,c,d){use(c+d)};f(...alert());f(4,3,2,1)",
        "function f(a,b,c,d){use(c+d)};f(...alert());f(0,0,2,1)");
    test(
        "function f(a,b,c,d){use(c+d)};f(1,...alert());f(4,3,2,1)",
        "function f(  b,c,d){use(c+d)};f(  ...alert());f(  0,2,1)");
    test(
        "function f(a,b,c,d){use(c+d)};f(1,2,...alert());f(4,3,2,1)",
        "function f(    c,d){use(c+d)};f(    ...alert());f(    2,1)");
    test(
        "function f(a,b,c,d){use(c+d)}; f(...[alert()],2,3);f(4,3,2,1)",
        "function f(a,b,c,d){use(c+d)}; f(...[alert()],2,3);f(0,0,2,1)");
  }

  @Test
  public void testFunctionArgRemovalFromCallSitesRest() {
    // remove all function arguments
    test(
        "var b=function(c,...d){return};b(1,2,3);b(4,5,6)",
        "var b=function(      ){return};b(     );b(     )");

    // remove no function arguments
    testSame("var b=function(c,...d){return c+d};b(1,2,3);use(b(4,5,6))");

    // remove some function arguments
    test(
        "var b=function(e,f,...c){return c};b(1,2,3,4);use(b(4,3,2,1))",
        "var b=function(    ...c){return c};b(    3,4);use(b(    2,1))");
  }

  @Test
  public void testFunctionArgRemovalFromCallSitesDefaultValue() {
    // remove all function arguments
    test(
        "function f(c = 1, d = 2){};f(1,2,3);f(4,5,6)",
        "function f(            ){};f(     );f(     )");
    test(
        "function f(c = alert()){};f(undefined);f(4)",
        "function f(c = alert()){};f(         );f(4)");
    test(
        "function f(c = alert()){};f();f()", //
        "function f(){var c = alert();};f();f()");
    // TODO(johnlenz): handle this like the "no value" case above and
    // allow the default value to inlined into the body.
    test(
        "function f(c = alert()){};f(undefined);f(undefined)",
        "function f(c = alert()){};f(          );f(        )");
  }

  @Test
  public void testFunctionArgRemovalFromCallSitesDestructuring() {
    // remove all function arguments
    test(
        "function f([a] = [1], [b] = [2]){} f(1, 2, 3); f(4, 5, 6)",
        "function f(                    ){} f(       ); f(       )");
    test(
        "function f(a, [b] = alert(), [c] = alert(), d){} f(1, 2, 3, 4); f(4, 5, 6, 7)",
        "function f(   [ ] = alert(), [ ] = alert()   ){} f(   2, 3   ); f(   5, 6   )");

    test(
        "function f(a, [b = alert()] = [], [c = alert()] = [], d){} f(1, 2, 3, 4);f(4, 5, 6, 7)",
        "function f(   [b = alert()] = [], [c = alert()] = []   ){} f(   2, 3   );f(   5, 6   )");

    test(
        "function f(a, [b = alert()], [c = alert()], d){} f(1, 2, 3, 4); f(4, 5, 6, 7);",
        "function f(   [b = alert()], [c = alert()]   ){} f(   2, 3   ); f(   5, 6   );");
  }

  @Test
  public void testLocalVarReferencesGlobalVar() {
    test(
        "var a=3;function f(b, c){b=a; alert(c);} f(1,2);f()",
        "function f(c) { alert(c); } f(2);f();");
  }

  @Test
  public void testReflectedMethods() {
    testSame(
        """
        /** @constructor */
        function Foo() {}
        Foo.prototype.handle = function(x, y) { alert(y); };
        var x = goog.reflect.object(Foo, {handle: 1});
        for (var i in x) { x[i].call(x); }
        window['Foo'] = Foo;
        """);
  }

  @Test
  public void testExistenceOfAGetter_preventsParamOptimization() {
    testSame(
        """
        class Bar {
        // TODO(nickreid): Use `declareAccessor rather than specifying in snippet. We can't do
        // that currently because `RemoveUnusedCode` unilaterally runs another collection.
          get foo() {
            return (x) => x;
          }
        }

        class Foo {
          foo() {}
        }

        new (Foo || Bar)().foo('onlyUsedByGetter');
        """);
  }

  @Test
  public void testExistenceOfASetter_preventsParamOptimization() {
    testSame(
        """
        class Bar {
        // TODO(nickreid): Use `declareAccessor rather than specifying in snippet. We can't do
        // that currently because `RemoveUnusedCode` unilaterally runs another collection.
          set foo(f) {
            this.bar = f;
          }
        }

        // This is a defnition for `.bar`. It would be dangerous to optimize it as if it were a
        // definition of `.foo`
        var x = new Bar();
        x.foo = function(param) { return param; };
        x.foo();
        """);
  }

  @Test
  public void testUndefinedLiterals_beforeOtherUnused() {
    test(
        """
        function foo(a, b) { use(a); }
        // check that this undefined gets removed if it is before another unused param
        foo(undefined, 3);
        foo(void 0, 4);
        """,
        """
        function foo(a) { use(a); }
        foo();
        foo();
        """);
  }
}
